Skip to content

feat(formula,email): render datetime in reference timezone (ADR-0053 Phase 2) (#1981)#2003

Merged
os-zhuang merged 2 commits into
mainfrom
feat/tz-datetime-render-1981
Jun 17, 2026
Merged

feat(formula,email): render datetime in reference timezone (ADR-0053 Phase 2) (#1981)#2003
os-zhuang merged 2 commits into
mainfrom
feat/tz-datetime-render-1981

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of ADR-0053 Phase 2 (Slice 4 of 6 — render-tz). Closes #1981. Design: #1975 · Parent: #1928.

Shows datetime instants in a reference timezone at the presentation boundary. Storage stays UTC. This is the last of the six Phase 2 slices.

Changes

Centralized path — formula template engine (low blast radius)

  • The datetime formatter takes the reference timezone from EvalContext.timezone (threaded in ADR-0053 Phase 2 · Slice 3: timezone-aware today()/daysFromNow()/daysAgo() #1980) and passes it to Intl.DateTimeFormat. {{ ts | datetime }} now renders in that zone; {{ ts | datetime:iso }} stays UTC (machine-readable, unambiguous).
  • Calendar-day date rendering is intentionally unchanged — a Field.date is tz-naive, so applying a zone would shift the day (acceptance criterion 3).
  • New exported formatValue(name, value, arg, { locale, timeZone }) — the single source of truth for value→string semantics, reusable by renderers that don't run the full CEL template engine.

Outlier — email pipeline (the architectural cost)

plugin-email's renderer bypassed the formatter pipeline entirely (String() only), so a datetime was emailed as raw ISO. Now:

  • Email holes accept the shared formula formatters — {{ order.total | currency }}, {{ ts | datetime }} — by reusing formatValue (so email formatting is identical to in-app). The engine keeps its HTML-escaping + {{{ }}} raw-output semantics and its lenient "never throw on render" contract (unknown formatter → raw value).
  • SendTemplateInput.timezone (mirroring the existing locale) flows into rendering, so the caller supplies the recipient's / tenant's zone (typically from ExecutionContext.timezone).
  • plugin-email gains a @objectstack/formula dependency (acyclic; formula is a leaf).

Acceptance criteria

  • {{ value | datetime }} renders in the reference tz when set; UTC/runtime when unset.
  • An email template containing a datetime renders in the reference tz (not raw UTC/ISO).
  • date (calendar-day) rendering is unaffected (tz-naive after Phase 1).

Tests

  • template-formatters.test.ts — datetime in NY vs UTC; datetime:iso stays UTC; date:iso unaffected; formatValue reuse + unknown-formatter undefined.
  • plugin-email/template-engine.test.ts — formatter holes, tz datetime, escaping preserved ({{ }} vs {{{ }}}), unknown-formatter fallback, missing value → ''.
  • send-template.test.ts — end-to-end: an email datetime renders in input.timezone, not raw ISO.
  • Suites green: formula 125, plugin-email 55. DTS builds type-check clean.

Depends on

Slice 1 (timezone value #1978) and #1980 — both merged.

🤖 Generated with Claude Code

…Phase 2, #1981)

datetime template holes render in a reference timezone's wall-clock at the
presentation boundary; storage stays UTC. Calendar-day `date` rendering is
intentionally unaffected (tz-naive).

- formula: the `datetime` formatter takes tz from EvalContext.timezone (#1980)
  → Intl.DateTimeFormat; `datetime:iso` stays UTC. New exported
  `formatValue(name, value, arg, { locale, timeZone })` so the whitelisted
  formatters are reusable outside the CEL template engine.
- plugin-email: the renderer bypassed the formatter pipeline (raw String()),
  so a datetime went out as raw ISO. Email holes now accept the shared formula
  formatters via formatValue (single source of truth), keeping HTML-escaping
  and `{{{ }}}` raw-output. SendTemplateInput.timezone (mirroring locale) flows
  into rendering so a datetime shows the recipient's wall-clock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 17, 2026 5:01am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies Pull requests that update a dependency file tests tooling size/m labels Jun 17, 2026
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/formula, @objectstack/plugin-email, @objectstack/spec.

89 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/cloud-artifact-api.mdx (via packages/spec)
  • content/docs/concepts/cluster-semantics.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/implementation-status.mdx (via @objectstack/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/concepts/packages.mdx (via @objectstack/formula, @objectstack/plugin-email, @objectstack/spec)
  • content/docs/concepts/setup-app.mdx (via @objectstack/spec)
  • content/docs/concepts/skills.mdx (via @objectstack/spec)
  • content/docs/concepts/webhook-delivery.mdx (via @objectstack/spec)
  • content/docs/getting-started/architecture.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/spec)
  • content/docs/getting-started/core-concepts.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/guides/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/guides/ai-capabilities.mdx (via @objectstack/spec)
  • content/docs/guides/airtable-dashboard-analysis.mdx (via @objectstack/spec)
  • content/docs/guides/analytics-datasets.mdx (via @objectstack/spec)
  • content/docs/guides/api-reference.mdx (via @objectstack/spec)
  • content/docs/guides/business-logic.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/error-catalog.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/field-type-gallery.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/field-validation-rules.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/protocol-diagram.mdx (via packages/spec)
  • content/docs/guides/cheatsheets/query-cheat-sheet.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/quick-reference.mdx (via @objectstack/spec)
  • content/docs/guides/client-sdk.mdx (via @objectstack/spec)
  • content/docs/guides/common-patterns.mdx (via @objectstack/spec)
  • content/docs/guides/contracts/auth-service.mdx (via packages/spec)
  • content/docs/guides/contracts/cache-service.mdx (via packages/spec)
  • content/docs/guides/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/guides/contracts/index.mdx (via @objectstack/spec)
  • content/docs/guides/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/guides/contracts/storage-service.mdx (via packages/spec)
  • content/docs/guides/data-modeling.mdx (via @objectstack/spec)
  • content/docs/guides/deployment-vercel.mdx (via @objectstack/spec)
  • content/docs/guides/driver-configuration.mdx (via @objectstack/spec)
  • content/docs/guides/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/guides/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/guides/formula.mdx (via @objectstack/formula, @objectstack/spec)
  • content/docs/guides/hook-bodies.mdx (via packages/spec)
  • content/docs/guides/kernel-services.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/dashboard.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/field.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/flow.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/index.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/object.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/validation.mdx (via @objectstack/formula, @objectstack/spec)
  • content/docs/guides/metadata/workflow.mdx (via @objectstack/spec)
  • content/docs/guides/packages.mdx (via @objectstack/formula, @objectstack/plugin-email, @objectstack/spec)
  • content/docs/guides/plugin-development.mdx (via @objectstack/spec)
  • content/docs/guides/plugins.mdx (via @objectstack/spec)
  • content/docs/guides/project-scoping.mdx (via @objectstack/spec)
  • content/docs/guides/public-forms.mdx (via @objectstack/spec)
  • content/docs/guides/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/guides/runtime-services/index.mdx (via packages/spec)
  • content/docs/guides/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/guides/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/guides/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/guides/security.mdx (via @objectstack/spec)
  • content/docs/guides/seed-data.mdx (via @objectstack/spec)
  • content/docs/guides/skills.mdx (via @objectstack/spec)
  • content/docs/guides/standards.mdx (via @objectstack/spec)
  • content/docs/guides/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/formula, @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Comment thread packages/plugins/plugin-email/src/template-engine.ts Fixed
The formatter-aware regex used two overlapping `\s*` groups (inside the
optional `| formatter` group and trailing), which CodeQL flagged as
js/polynomial-redos (slow on `{{{{.` + many tabs). Switch to a brace-free
inner capture (`[^{}]*` — single star, no nested quantifier) and parse the
hole body with plain string ops, the same linear strategy the formula
template engine uses. Adds a regression test on the flagged input shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@os-zhuang
os-zhuang merged commit 575448d into main Jun 17, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the feat/tz-datetime-render-1981 branch June 17, 2026 05:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0053 Phase 2 · Slice 4: render datetime in reference timezone (formatters + email)

2 participants